package server.java.autoduel.World;

import java.awt.Point;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import server.java.autoduel.Server;
import server.java.autoduel.Network.IPacketizable;
import server.java.autoduel.Network.OPCODES;
import server.java.autoduel.Player.Player;

import com.sun.sgs.app.AppContext;
import com.sun.sgs.app.DataManager;
import com.sun.sgs.app.Delivery;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.ManagedReference;
import com.thoughtworks.xstream.XStream;

/**
 * Represents the autoduel world
 * 
 * @author Ian Axelrod
 * @author Liz Ha
 * @author Andrew Muldowney
 * @author Edward Futch
 */
public class World implements Serializable, ManagedObject, IPacketizable {
	private static final long serialVersionUID = -340992522683122843L;

	/**
	 * The number of grid columns
	 */
	public static final int GRIDS_X = 5;

	/**
	 * The number of grid rows
	 */
	public static final int GRIDS_Y = 5;

	/**
	 * The logger for this class
	 */
	private static final Logger logger = Logger
			.getLogger(World.class.getName());

	/**
	 * All the players in the Autoduel world
	 */
	private final Map<String, ManagedReference<Player>> playerRefs = new HashMap<String, ManagedReference<Player>>();

	/**
	 * The set of grids for this world
	 */
	private final Map<Integer, ManagedReference<Grid>> gridRefs = new HashMap<Integer, ManagedReference<Grid>>();

	/**
	 * Location of the XML file that holds grid information for this world
	 */
	private final String gridsLoc;

	/**
	 * Constructs a World object
	 * 
	 * @param gridLoc
	 *            the location for the grid xml file
	 */
	public World(String gridsLoc) {
		World.logger.setLevel(Server.loggerLevel);
		this.gridsLoc = gridsLoc;
	}

	/**
	 * Adds a player to the world
	 * 
	 * @param player
	 *            the player to add
	 */
	public void addPlayer(Player player) {
		World.logger.info("Player " + player.getName() + " entered world.");
		final DataManager dataManager = AppContext.getDataManager();

		dataManager.markForUpdate(this);

		// add the player to the appropriate grid
		getGridFromCoord(player.getCurrentCoord()).addPlayer(player);

		// add the player to the world's player references list
		this.playerRefs.put(player.getName(),
				dataManager.createReference(player));
	}

	/**
	 * Makes a copy of the world
	 * 
	 * @return a copy of the world
	 */
	public World copy() {
		return new World(this.gridsLoc);
	}

	@Override
	public ByteBuffer encodePacket(OPCODES opcode) {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * Return a set of grids around a target grid
	 * 
	 * @param grid
	 *            the target grid
	 * @return a set of grids around the target grid
	 */
	public Grid[] getAdjacentGrids(Grid grid) {
		final Point coord = grid.getCoord();
		final Point[] adjCoords = {
				new Point(coord.x - Grid.Width, coord.y - Grid.Height), // Grid
																		// 0
				new Point(coord.x, coord.y - Grid.Height), // Grid 1
				new Point(coord.x + Grid.Width, coord.y - Grid.Height), // Grid
																		// 2
				new Point(coord.x - Grid.Width, coord.y), // Grid 3
				new Point(coord.x + Grid.Width, coord.y), // Grid 5
				new Point(coord.x - Grid.Width, coord.y + Grid.Height), // Grid
																		// 6
				new Point(coord.x, coord.y + Grid.Height), // Grid 7
				new Point(coord.x + Grid.Width, coord.y + Grid.Height) // Grid
		}; // 8
		final ArrayList<Grid> arr = new ArrayList<Grid>();

		// create a list of all grids adjacent to coord
		final Point tempPoint = new Point(0, 0);
		for (final Point c : adjCoords) {
			/*
			 * logger.severe("Current Point: "+c.toString()); //Wrap Around
			 * checking if(c.x < 0){
			 * logger.severe("CP: X < 0: "+c.x+" += "+Grid.
			 * Width*(World.GRIDS_X+1)); c.x += (Grid.Width*(World.GRIDS_X+1));
			 * } if(c.x > Grid.Width*(World.GRIDS_X+1)){
			 * logger.severe("CP: X > MAX: "
			 * +c.x+" -= "+Grid.Width*(World.GRIDS_X+1)); c.x -=
			 * Grid.Width*(World.GRIDS_X+1); } if(c.y < 0){
			 * logger.severe("CP: Y < 0: "
			 * +c.y+" += "+Grid.Height*(World.GRIDS_Y+1)); c.y +=
			 * (Grid.Height*World.GRIDS_Y+1); } if(c.y >
			 * Grid.Height*(World.GRIDS_Y+1)){
			 * logger.severe("CP: Y > MAX: "+c.y+
			 * " -= "+Grid.Height*(World.GRIDS_Y+1)); c.y -=
			 * Grid.Height*(World.GRIDS_Y+1); }
			 */
			final Grid g = getGridFromCoord(c);
			if (g != null) {
				/*
				 * if (pivot.x > c.x && pivot.y > c.y) { g.setLocalIndex(new
				 * Point(0,0)); } else if (pivot.x == c.x && pivot.y > c.y) {
				 * g.setLocalIndex(new Point(0,1)); } else if (pivot.x > c.x &&
				 * pivot.y == c.y) { g.setLocalIndex(new Point(1,0)); } else if
				 * (pivot.x == c.x && pivot.y == c.y) { g.setLocalIndex(new
				 * Point(1,1)); } else if (pivot.x < c.x && pivot.y > c.y) {
				 * g.setLocalIndex(new Point(0,2)); } else if (pivot.x < c.x &&
				 * pivot.y == c.y) { g.setLocalIndex(new Point(1, 2)); } else if
				 * (pivot.x > c.x && pivot.y < c.y) { g.setLocalIndex(new
				 * Point(2, 0)); } else if (pivot.x == c.x && pivot.y < c.y) {
				 * g.setLocalIndex(new Point(2, 1)); } else {
				 * g.setLocalIndex(new Point(2, 2)); }
				 */
				World.logger.info("Setting " + g.toString() + ".index to ("
						+ tempPoint.x + "," + tempPoint.y + ")");
				g.setLocalIndex(tempPoint);

				tempPoint.x++;
				if ((tempPoint.x == 1) && (tempPoint.y == 1)) {
					tempPoint.x++;
				}
				if (tempPoint.x > 2) {
					tempPoint.x = 0;
					tempPoint.y++;
				}
				World.logger.info("Found adjacent grid: " + grid.toString());
				arr.add(g);
			}
		}

		Grid[] grids = new Grid[arr.size()];
		grids = arr.toArray(grids);

		return grids;
	}

	/**
	 * Return the grid associated with this coordinate
	 * 
	 * @param coord
	 *            the coordinate
	 * @return the grid where the coordinate lies
	 */
	public Grid getGridFromCoord(Point coord) {
		World.logger.info("*******************Grid Trying to Retrieve: "
				+ coord.toString());
		if ((coord.x / Grid.Width) >= World.GRIDS_X) {
			World.logger.info("Too Big X");
			coord.x = 0;
		}
		if ((coord.y / Grid.Height) >= World.GRIDS_Y) {
			World.logger.info("Too Big Y");
			coord.y = 0;
		}
		if (coord.y < 0) {
			World.logger.info("Too Small Y");
			coord.y = Grid.Height * (World.GRIDS_Y - 1);
		}
		if (coord.x < 0) {
			World.logger.info("Too Small X");
			coord.x = Grid.Width * (World.GRIDS_X - 1);
		}
		World.logger.info("*******************Grid Converted To: "
				+ coord.toString());
		// convert 2D index to 1D index
		final int cols = (coord.x / Grid.Width);
		final int rows = (coord.y / Grid.Height);
		final int index = rows * World.GRIDS_X + cols;
		World.logger.info("Col: " + cols + "  Row: " + rows);
		final ManagedReference<Grid> gridRef = this.gridRefs.get(index);
		Grid grid = null;

		if (gridRef != null) {
			grid = gridRef.get();
			if (grid.isEmptyGrid()) {
				World.logger.warning("Coordinate " + coord
						+ " resulted in an EMPTY grid.");
			}
			World.logger.info("Grid: " + grid + " was returned from index: "
					+ index);
		} else {
			World.logger.info("Coordinate " + coord
					+ " resulted in a NULL grid!");
		}

		return grid;
	}

	public Player getPlayerByName(String name) {
		final ManagedReference<Player> playerRef = this.playerRefs.get(name);

		if (playerRef != null) {
			return playerRef.get();
		} else {
			return null;
		}
	}

	/**
	 * @return the playerRefs
	 */
	public Map<String, ManagedReference<Player>> getPlayerRefs() {
		return this.playerRefs;
	}

	/**
	 * Gets all players that can be seen by the target player
	 * 
	 * @param player
	 *            the target player
	 * @return a list of players
	 */
	public Player[] getPlayersInView(Player player) {
		final Grid grid = getGridFromCoord(player.getCurrentCoord());
		if (grid != null) {
			final Grid[] grids = getAdjacentGrids(grid);
			final ArrayList<Player> players = new ArrayList<Player>();

			// create a list of all players in the current grid
			for (final Player p : grid.getPlayers()) {
				players.add(p);
			}

			// add to the list all the players in adjacent grids
			for (final Grid g : grids) {
				for (final Player p : g.getPlayers()) {
					players.add(p);
				}
			}

			Player[] playersArr = new Player[players.size()];
			playersArr = players.toArray(playersArr);

			return playersArr;
		} else {
			return null;
		}
	}

	/**
	 * Takes a set of grids from the specified xml file and creates references
	 * to them
	 * 
	 * @param currentDir
	 *            the current directory
	 * @throws IOException
	 */
	public void loadGrids(String currentDir) throws IOException {
		final File gridsFile = new File(currentDir + this.gridsLoc);
		final FileInputStream reader = new FileInputStream(gridsFile);
		final byte[] buf = new byte[(int) gridsFile.length()];

		reader.read(buf);
		reader.close();
		final XStream xstream = new XStream();
		xstream.alias("Grid", Grid.class);
		xstream.alias("Array", List.class);
		@SuppressWarnings("unchecked")
		final List<Grid> grids = (List<Grid>) xstream.fromXML(new String(buf));
		int h = 0;
		int w = 0;
		Grid.logger.setLevel(Server.loggerLevel);
		for (final Grid grid : grids) {
			grid.setCoord(new Point(Grid.Width * w, Grid.Height * h));
			w++;
			if (w >= World.GRIDS_X) {
				h++;
				w = 0;
			}

			World.logger.info("Loaded grid  from xml: " + grid.toString());

			grid.loadLocs(currentDir);
			grid.loadObjs(currentDir);

			AppContext.getChannelManager().createChannel(
					grid.getCoord().toString(), null, Delivery.RELIABLE);
			World.logger.info("Created channel for grid: " + grid.toString());
		}
		final DataManager dataManager = AppContext.getDataManager();
		dataManager.markForUpdate(this);

		for (int i = 0; i < grids.size(); i++) {
			this.gridRefs.put(i, dataManager.createReference(grids.get(i)));
		}
	}

	/**
	 * Removes a player from the world
	 * 
	 * @param player
	 *            the player to remove
	 */
	public void removePlayer(Player player) {
		final DataManager dataManager = AppContext.getDataManager();

		dataManager.markForUpdate(this);

		// remove the player from the appropriate grid
		getGridFromCoord(player.getCurrentCoord()).removePlayer(player);

		// remove the player from the world's player references list
		this.playerRefs.remove(player.getName());
	}

	@Override
	public String toString() {
		return "GridsLoc: " + this.gridsLoc;
	}
}
